Java のコンパレータと比較について書くのは怠け者だけではありません。私は怠け者ではないので、別の説明について気に入って不満を言ってください。余計なものにならないことを願っています。そして、はい、この記事は「記憶からコンパレータを書くことができますか? 」という質問に対する答えです。この記事を読んだ後は、誰もが記憶からコンパレータを書けるようになることを願っています。

序章
ご存知のとおり、Java はオブジェクト指向言語です。そのため、オブジェクトを Java で操作するのが一般的です。しかし、遅かれ早かれ、何らかの特性に基づいてオブジェクトを比較するというタスクに直面することになります。 例: クラスによって記述されたメッセージがあるとしますMessage
。
public static class Message {
private String message;
private int id;
public Message(String message) {
this.message = message;
this.id = new Random().nextInt(1000);
}
public String getMessage() {
return message;
}
public Integer getId() {
return id;
}
public String toString() {
return "[" + id + "] " + message;
}
}
このクラスをTutorialspoint Java コンパイラに配置します。import ステートメントも忘れずに追加してください。
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
メソッド内でmain
、いくつかのメッセージを作成します。
public static void main(String[] args){
List<Message> messages = new ArrayList();
messages.add(new Message("Hello, World!"));
messages.add(new Message("Hello, Sun!"));
System.out.println(messages);
}
それらを比較したい場合はどうするかを考えてみましょう。たとえば、ID で並べ替えたいとします。そして、順序を作成するには、どのオブジェクトが最初に来るべきか (つまり、小さいオブジェクト)、どのオブジェクトが後に来るべきか (つまり、大きいオブジェクト) を理解するために、何らかの方法でオブジェクトを比較する必要があります。java.lang.Objectのようなクラスから始めましょう。すべてのクラスが暗黙的にそのObject
クラスを継承することがわかっています。これは、「すべてはオブジェクトである」という概念を反映しており、すべてのクラスに共通の動作を提供するため、理にかなっています。このクラスは、すべてのクラスに 2 つのメソッドがあることを指示します。 →hashCode
メソッドhashCode
は数値を返します (int
) オブジェクトの表現。どういう意味ですか?これは、クラスの 2 つの異なるインスタンスを作成する場合、それらには異なる が必要であることを意味しますhashCode
。メソッドの説明には、「合理的に実用的である限り、Object クラスによって定義された hashCode メソッドは、個別のオブジェクトに対して個別の整数を返します。」と同様のことが書かれています。言い換えれば、2 つの異なるinstance
s には、異なる s が存在する必要がありますhashCode
。つまり、この方法は比較には適していません。→ equals
。このequals
メソッドは、「これらのオブジェクトは等しいか?」という質問に答えます。を返しますboolean
。」 デフォルトでは、このメソッドには次のコードが含まれます。
public boolean equals(Object obj) {
return (this == obj);
}
つまり、このメソッドがオーバーライドされない場合、本質的にはオブジェクト参照が一致するかどうかを示します。私たちが関心があるのはオブジェクト参照ではなくメッセージ ID であるため、これはメッセージに望んでいることではありません。そして、たとえequals
メソッドをオーバーライドしたとしても、私たちが期待できるのは、それらが等しいかどうかを知ることだけです。そして、これだけでは順序を決定するのに十分ではありません。それでは何が必要なのでしょうか?比較できるものが必要です。比較するのは ですComparator
。Java APIを開き、Comparator を見つけます。確かにjava.util.Comparator
インターフェイス はありますjava.util.Comparator and java.util.Comparable
ご覧のとおり、このようなインターフェイスが存在します。それを実装するクラスは、「オブジェクトを比較するメソッドを実装します」と言います。本当に覚えておく必要があるのは、次のように表現されるコンパレータ コントラクトだけです。
Comparator returns an int according to the following rules:
- It returns a negative int if the first object is smaller
- It returns a positive int if the first object is larger
- It returns zero if the objects are equal
それではコンパレータを書いてみましょう。をインポートする必要がありますjava.util.Comparator
。import ステートメントの後に、次のコードをメソッドに追加しますmain
。 Comparator<Message> comparator = new Comparator<Message>();
もちろん、これはComparator
インターフェイスであるため、機能しません。{}
したがって、括弧の後に中括弧を追加します。次のメソッドを中括弧内に記述します。
public int compare(Message o1, Message o2) {
return o1.getId().compareTo(o2.getId());
}
スペルを覚える必要さえありません。コンパレーターとは、比較を実行する、つまり比較する人です。オブジェクトの相対的な順序を示すために、 を返しますint
。基本的にはそれだけです。素敵で簡単です。例からわかるように、Comparator に加えて別のインターフェイスがあり、メソッドjava.lang.Comparable
を実装する必要がありますcompareTo
。このインターフェイスは、「me を実装するクラスにより、クラスのインスタンスを比較できるようになります」と述べています。たとえば、Integer
の To の実装はcompare
次のとおりです。
(x < y) ? -1 : ((x == y) ? 0 : 1)
Java 8 では、いくつかの優れた変更が導入されました。インターフェイスをよく見ると、その上に注釈がComparator
表示されます。@FunctionalInterface
この注釈は情報提供を目的としており、このインターフェイスが機能していることを示します。これは、このインターフェイスには実装のないメソッドである抽象メソッドが 1 つだけあることを意味します。これは私たちに何をもたらすのでしょうか?これで、コンパレータのコードを次のように記述できます。
Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
括弧内に変数の名前を付けます。Java は、メソッドが 1 つしかないため、必要な入力パラメータの数とタイプが明確であることを認識します。次に、矢印演算子を使用して、それらをコードのこの部分に渡します。さらに、Java 8 のおかげで、インターフェイスにデフォルトのメソッドが追加されました。これらのメソッドは、インターフェイスを実装するときにデフォルトで表示されます。インターフェースComparator
にはいくつかあります。例えば:
Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
コードをきれいにする別の方法があります。コンパレータを定義した上記の例を見てください。それは何をするためのものか?かなり原始的なんです。これは単にオブジェクトを取得し、「比較可能な」値を抽出するだけです。たとえば、Integer
を実装しているcomparable
ため、メッセージ ID フィールドの値に対して CompareTo 操作を実行できます。この単純なコンパレータ関数は次のように記述できます。
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
言い換えれば、次Comparator
のように比較する があります。オブジェクトを取得し、getId()
メソッドを使用してオブジェクトから を取得しComparable
、その後、 を使用してcompareTo
比較します。そして、これ以上恐ろしい構造物はありません。そして最後に、もう 1 つの特徴について触れておきたいと思います。コンパレータは連鎖させることができます。例えば:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());
応用
コンパレータの宣言は非常に論理的だと思いませんか? 次に、それをどこでどのように使用するかを確認する必要があります。→Collections.sort(java.util.Collections)
もちろん、この方法でコレクションを並べ替えることもできます。ただし、すべてのコレクションではなく、リストのみです。リストはインデックスによって要素にアクセスする種類のコレクションであるため、ここでは何も珍しいことではありません。これにより、2 番目の要素を 3 番目の要素と交換できるようになります。そのため、次の並べ替え方法はリストのみに適用されます。
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
→Arrays.sort(java.util.Arrays)
配列もソートしやすい。繰り返しますが、同じ理由で、それらの要素はインデックスによってアクセスされます。→Descendants of java.util.SortedSet and java.util.SortedMap
思い出してください。要素が格納される順序は保証されませんSet
。Map
ただし、順序を保証する特別な実装があります。コレクションの要素が を実装していない場合は、 をそのコンストラクターに java.util.Comparable
渡すことができます。Comparator
Set<Message> msgSet = new TreeSet(comparator);
→ Stream API
Java 8 で登場した Stream API では、コンパレータを使用してストリーム要素の操作を簡素化できます。たとえば、0 から 999 までの一連の乱数が必要だとします。
Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
.limit(10)
.sorted(Comparator.naturalOrder())
.forEach(e -> System.out.println(e));
ここで終わることもできますが、さらに興味深い問題があります。たとえば、Map
キーがメッセージ ID である を準備する必要があるとします。さらに、これらのキーを並べ替えたいので、次のコードから始めます。
Map<Integer, Message> collected = Arrays.stream(messages)
.sorted(Comparator.comparing(msg -> msg.getId()))
.collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
実際にここを取得しますHashMap
。そしてご存知のとおり、順序が保証されるわけではありません。その結果、ID で並べ替えられた要素の順序が失われます。良くない。コレクターを少し変更する必要があります。
Map<Integer, Message> collected = Arrays.stream(messages)
.sorted(Comparator.comparing(msg -> msg.getId()))
.collect(Collectors.toMap(msg -> msg.getId(), msg -> msg, (oldValue, newValue) -> oldValue, TreeMap::new));
コードは少し怖くなってきましたが、問題は正しく解決されました。さまざまなグループ分けの詳細については、こちらをご覧ください。
独自のコレクターを作成できます。詳細については、「Java 8 でのカスタム コレクターの作成」を参照してください。ここでの議論「ストリームにマップする Java 8 リスト」を読むと有益です。
フォールトラップ
Comparator
そしてComparable
良いです。ただし、覚えておくべきニュアンスが 1 つあります。クラスが並べ替えを実行するとき、クラスはクラスを に変換できることを期待しますComparable
。そうでない場合は、実行時にエラーが発生します。例を見てみましょう:
SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
ここでは何も問題がないようです。しかし、実際には、この例では、次のエラーで失敗します。 java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable
要素 (結局SortedSet
、 です) を並べ替えようとしたためです...しかし、できませんでした。SortedMap
とを使用するときは、これを忘れないでくださいSortedSet
。
さらに読む: |
---|
GO TO FULL VERSION