1. 概要

Java開発者として、コレクションにグループ化された要素をソートする必要があることがよくあります。 Javaを使用すると、あらゆるタイプのデータを使用してさまざまな並べ替えアルゴリズムを実装できます

たとえば、文字列をアルファベット順、アルファベット逆順、または長さに基づいて並べ替えることができます。

このチュートリアルでは、CompareableインターフェースとそのcompareToメソッドを調べます。これによりソートが可能になります。 コアクラスとカスタムクラスの両方のオブジェクトを含むコレクションの並べ替えについて見ていきます。

また、 compareTo を適切に実装するためのルール、および回避する必要のある壊れたパターンについても説明します。

2. 比較可能なインターフェース

比較可能なインターフェースは、それを実装する各クラスのオブジェクトに順序付けを課します

compareTo は、Compareableインターフェースで定義されている唯一のメソッドです。 これは、自然比較法と呼ばれることがよくあります。

2.1. compareToの実装

compareTo メソッドは、現在のオブジェクトをパラメーターとして送信されたオブジェクトと比較します。

それを実装するとき、メソッドが以下を返すことを確認する必要があります。

  • 現在のオブジェクトがパラメータオブジェクトより大きい場合は、正の整数
  • 現在のオブジェクトがパラメータオブジェクトよりも小さい場合は、負の整数
  • 現在のオブジェクトがパラメータオブジェクトと等しい場合はゼロ

数学では、これを符号または符号関数と呼びます。

2.2. 実装例

compareToメソッドがコアIntegerクラスにどのように実装されているかを見てみましょう。

@Override
public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

public static int compare (int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

2.3. 壊れた減算パターン

代わりに、巧妙な減算ワンライナーを使用できると主張する人もいるかもしれません。

@Override
public int compareTo(BankAccount anotherAccount) {
    return this.balance - anotherAccount.balance;
}

正の口座残高が負の口座残高よりも大きいと予想される例を考えてみましょう。

BankAccount accountOne = new BankAccount(1900000000);
BankAccount accountTwo = new BankAccount(-2000000000);
int comparison = accountOne.compareTo(accountTwo);
assertThat(comparison).isNegative();

ただし、整数は差を格納するのに十分な大きさではないため、間違った結果が得られます。 確かに、このパターンは整数のオーバーフローの可能性があるために壊れており、回避する必要があります

正しい解決策は、減算の代わりに比較を使用することです。 コアIntegerクラスからの正しい実装を再利用することもできます。

@Override
public int compareTo(BankAccount anotherAccount) {
    return Integer.compare(this.balance, anotherAccount.balance);
}

2.4. 実装ルール

compareTo メソッドを適切に実装するには、次の数学的規則を尊重する必要があります。

  • sgn(x.compareTo(y))== -sgn(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))を意味します

また、必須ではありませんが、compareTo実装を equals メソッド実装と一貫性を保つようにすることを強くお勧めします。

  • x.compareTo(e2)== 0x.equals(y)と同じブール値を持つ必要があります

これにより、並べ替えられたセットと並べ替えられたマップでオブジェクトを安全に使用できるようになります。

2.5. との整合性はに等しい

compareToequalsの実装に一貫性がない場合に何が起こるかを見てみましょう。

この例では、 compareTo メソッドは得点されたゴールをチェックし、equalsメソッドはプレーヤー名をチェックしています。

@Override
public int compareTo(FootballPlayer anotherPlayer) {
    return this.goalsScored - anotherPlayer.goalsScored;
}

@Override
public boolean equals(Object object) {
    if (this == object)
        return true;
    if (object == null || getClass() != object.getClass())
        return false;
    FootballPlayer player = (FootballPlayer) object;
    return name.equals(player.name);
}

これにより、ソートされたセットまたはソートされたマップでこのクラスを使用すると、予期しない動作が発生する可能性があります。

FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 800);

TreeSet<FootballPlayer> set = new TreeSet<>();
set.add(messi);
set.add(ronaldo);

assertThat(set).hasSize(1);
assertThat(set).doesNotContain(ronaldo);

ソートされたセットは、[X82X] equals メソッドではなく、compareToを使用してすべての要素の比較を実行します。 したがって、2人のプレーヤーは、その観点からは同等であるように見え、2番目のプレーヤーは追加されません。

3. コレクションの並べ替え

Compareable インターフェースの主な目的は、コレクションまたは配列にグループ化された要素の自然な並べ替えを可能にすることです

JavaユーティリティメソッドCollections.sortまたはArrays.sortを使用して、Compareableを実装するすべてのオブジェクトを並べ替えることができます。

3.1. コアJavaクラス

String Integer Double などのほとんどのコアJavaクラスは、すでにCompareableインターフェースを実装しています。

したがって、既存の自然な並べ替えの実装を再利用できるため、並べ替えは非常に簡単です。

数値を自然な順序で並べ替えると、昇順になります。

int[] numbers = new int[] {5, 3, 9, 11, 1, 7};
Arrays.sort(numbers);
assertThat(numbers).containsExactly(1, 3, 5, 7, 9, 11);

一方、文字列を自然に並べ替えると、アルファベット順になります。

String[] players = new String[] {"ronaldo",  "modric", "ramos", "messi"};
Arrays.sort(players);
assertThat(players).containsExactly("messi", "modric", "ramos", "ronaldo");

3.2. カスタムクラス

対照的に、カスタムクラスを並べ替え可能にするには、Comparableインターフェイスを手動で実装する必要があります。

Compareable を実装していないオブジェクトのコレクションをソートしようとすると、Javaコンパイラはエラーをスローします。

配列で同じことを試してみると、コンパイル中に失敗することはありません。 ただし、クラスキャストランタイム例外が発生します。

HandballPlayer duvnjak = new HandballPlayer("Duvnjak", 197);
HandballPlayer hansen = new HandballPlayer("Hansen", 196);
HandballPlayer[] players = new HandballPlayer[] {duvnjak, hansen};
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> Arrays.sort(players));

3.3. TreeMapおよびTreeSet

TreeMapTreeSetは、Javaコレクションフレームワークの2つの実装であり、はそれらの要素の自動ソートを支援します

ソートされたマップでComparableインターフェイスを実装するオブジェクトを使用することも、ソートされたセットの要素として使用することもできます。

得点したゴール数に基づいてプレーヤーを比較するカスタムクラスの例を見てみましょう。

@Override
public int compareTo(FootballPlayer anotherPlayer) {
    return Integer.compare(this.goalsScored, anotherPlayer.goalsScored);
}

この例では、compareTo実装で定義された基準に基づいてキーが自動的にソートされます。

FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 900);
FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer modric = new FootballPlayer("modric", 100);

Map<FootballPlayer, String> players = new TreeMap<>();
players.put(ronaldo, "forward");
players.put(messi, "forward");
players.put(modric, "midfielder");

assertThat(players.keySet()).containsExactly(modric, messi, ronaldo);

4. コンパレータ代替

自然ソートに加えて、 Javaでは、特定の順序付けロジックを柔軟な方法で定義することもできます。

コンパレータインターフェースは、ソートしているオブジェクトから切り離された複数の異なる比較戦略を可能にします。

FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 900);
FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer modric = new FootballPlayer("Modric", 100);

List<FootballPlayer> players = Arrays.asList(ronaldo, messi, modric);
Comparator<FootballPlayer> nameComparator = Comparator.comparing(FootballPlayer::getName);
Collections.sort(players, nameComparator);

assertThat(players).containsExactly(messi, modric, ronaldo);

一般に、並べ替えるオブジェクトのソースコードを変更したくない、または変更できない場合にも適しています。

5. 結論

この記事では、 Comparableインターフェースを使用して、Javaクラスの自然なソートアルゴリズムを定義する方法について説明しました。 一般的な壊れたパターンを調べ、compareToメソッドを適切に実装する方法を定義しました。

また、コアクラスとカスタムクラスの両方を含むコレクションの並べ替えについても検討しました。 次に、ソートされたセットとソートされたマップで使用されるクラスでのcompareToメソッドの実装を検討しました。

最後に、代わりにコンパレータインターフェースを使用する必要がある場合のいくつかのユースケースを検討しました。

いつものように、ソースコードはGitHubから入手できます。