Java アドバンス

Java の高度なソート機能(Comparator と Comparable)

1. Java の高度なソート(Advanced Sorting)

以前の章では、リストをアルファベット順や数値順にソートする方法を学びました。しかし、リストの中にオブジェクトが含まれている場合はどうすればよいでしょうか?

オブジェクトをソートするには、それらをどのように並べるかを決定する「ルール」を指定する必要があります。例えば、車のリスト(Carオブジェクトのリスト)がある場合、製造年(year)でソートしたいかもしれません。その際のルールは「製造年が古い車を先にする」といったものになります。

Java の ComparatorComparable インターフェースを使用すると、オブジェクトのソートに使用するルールを自由に指定できます。

このソートルールを細かく定義できる機能は、文字列や数値のデフォルトの並び順を変更したい場合にも非常に有効です。

2. Comparator(比較器)

Comparator インターフェースを実装したオブジェクトを「コンパレータ」と呼びます。

このインターフェースを利用してクラスを作成し、compare() メソッドをオーバーライドすることで、2つのオブジェクトのどちらをリストの前方に配置するかを決定できます。

compare() メソッドが返す数値の意味は以下の通りです:

  • 負の数: 最初のオブジェクトを先に配置する。
  • 正の数: 2番目のオブジェクトを先に配置する。
  • ゼロ: 順序は問わない。

Comparator を実装するクラスは、以下のようになります。

// 製造年(year)で Car オブジェクトをソートするクラス
class SortByYear implements Comparator {
  public int compare(Object obj1, Object obj2) {
    // オブジェクトを Car 型にキャストする
    Car a = (Car) obj1;
    Car b = (Car) obj2;
    
    // オブジェクトを比較する
    if (a.year < b.year) return -1; // 最初の車の製造年の方が小さい
    if (a.year > b.year) return 1;  // 最初の車の製造年の方が大きい
    return 0; // 両方の車の製造年が同じ
  }
}

このコンパレータを使用するには、ソート用メソッドの引数として渡します。

// コンパレータを使用して車のリストをソートする
Comparator myComparator = new SortByYear();
Collections.sort(myCars, myComparator);

2.1 コンパレータを使用した完全なコード例

車のリストを製造年順にソートする完全なプログラムです。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

// Car クラスの定義
class Car {
  public String brand;
  public String model;
  public int year;
  
  public Car(String b, String m, int y) {
    brand = b;
    model = m;
    year = y;
  }
}

// コンパレータの作成
class SortByYear implements Comparator {
  public int compare(Object obj1, Object obj2) {
    // オブジェクトが Car 型であることを確認してキャスト
    Car a = (Car) obj1;
    Car b = (Car) obj2;
    
    // 両方のオブジェクトの製造年を比較
    if (a.year < b.year) return -1; // 最初の車の方が古い
    if (a.year > b.year) return 1;  // 最初の車の方が新しい
    return 0; // 同じ製造年
  }
}

public class Main { 
  public static void main(String[] args) { 
    // 車のリストを作成
    ArrayList<Car> myCars = new ArrayList<Car>();    
    myCars.add(new Car("BMW", "X5", 1999));
    myCars.add(new Car("Honda", "Accord", 2006));
    myCars.add(new Car("Ford", "Mustang", 1970));

    // コンパレータを使用してリストをソート
    Comparator myComparator = new SortByYear();
    Collections.sort(myCars, myComparator);

    // ソートされたリストを表示
    for (Car c : myCars) {
      System.out.println(c.brand + " " + c.model + " " + c.year);
    }
  }
}

3. ラムダ式の利用

コードをより簡潔にするために、コンパレータをラムダ式(Lambda Expression)に置き換えることができます。ラムダ式は compare() メソッドと同じ引数と戻り値を持つブロックとして機能します。

3.1 ラムダ式による実装例

// ラムダ式をコンパレータとして使用
Collections.sort(myCars, (obj1, obj2) -> {
  Car a = (Car) obj1;
  Car b = (Car) obj2;
  if (a.year < b.year) return -1;
  if (a.year > b.year) return 1;
  return 0;
});

4. 特殊なソート規則の作成

コンパレータを使用すれば、文字列や数値に対して独自のルールを適用することも可能です。以下の例では、奇数よりも「偶数を先に」リストアップするカスタムルールを作成しています。

4.1 偶数優先ソートの例

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

class SortEvenFirst implements Comparator {
  public int compare(Object obj1, Object obj2) {
    // オブジェクトを Integer 型にキャスト
    Integer a = (Integer)obj1;
    Integer b = (Integer)obj2;
    
    // 各数値が偶数かどうかを判定(2で割った余りが0なら偶数)
    boolean aIsEven = (a % 2) == 0;
    boolean bIsEven = (b % 2) == 0;
    
    if (aIsEven == bIsEven) {
      // 両方が偶数、または両方が奇数の場合は通常の昇順ルールを適用
      if (a < b) return -1;
      if (a > b) return 1;
      return 0;
    } else {
      // a が偶数なら先に配置(-1)、そうでなければ b(偶数)を先に配置
      if (aIsEven) {
      	return -1;
      } else {
        return 1;
      }
    }
  }
}

public class Main {
  public static void main(String[] args) {
    ArrayList<Integer> myNumbers = new ArrayList<Integer>();
    myNumbers.add(33);
    myNumbers.add(15);
    myNumbers.add(20);
    myNumbers.add(34);
    myNumbers.add(8);
    myNumbers.add(12);

    Comparator myComparator = new SortEvenFirst();
    Collections.sort(myNumbers, myComparator);

    for (int i : myNumbers) {
      System.out.println(i);
    }
  }
}

5. Comparable インターフェース

Comparable インターフェースを使用すると、オブジェクト自体に compareTo() メソッドを持たせ、独自のソートルールを内包させることができます。

compareTo() メソッドは別のオブジェクトを引数として受け取り、自身と比較して順序を決定します。

戻り値のルールは Comparator と同様です:

  • 負の数: 自身(this)を先に配置する。
  • 正の数: 引数のオブジェクトを先に配置する。
  • ゼロ: 順序は問わない。

Java の多くの標準クラス(StringInteger など)はこの Comparable インターフェースを実装しています。そのため、文字列や数値は明示的なコンパレータなしでソートできるのです。

5.1 Comparable を実装したクラスの例

class Car implements Comparable {
  public String brand;
  public String model;
  public int year;
  
  // このオブジェクトを他のオブジェクトとどう比較するかを定義
  public int compareTo(Object obj) {
    Car other = (Car)obj;
    if(year < other.year) return -1; // 自身の製造年の方が小さい
    if(year > other.year) return 1;  // 自身の製造年の方が大きい
    return 0; // 同じ
  }
}

5.2 Comparable による完全なコード例

import java.util.ArrayList;
import java.util.Collections;

// Comparable を実装した Car クラス
class Car implements Comparable {
  public String brand;
  public String model;
  public int year;
  
  public Car(String b, String m, int y) {
    brand = b;
    model = m;
    year = y;
  }
  
  // 比較ロジックの実装
  public int compareTo(Object obj) {
    Car other = (Car)obj;
    if(year < other.year) return -1;
    if(year > other.year) return 1;
    return 0;
  }
}

public class Main { 
  public static void main(String[] args) { 
    ArrayList<Car> myCars = new ArrayList<Car>();    
    myCars.add(new Car("BMW", "X5", 1999));
    myCars.add(new Car("Honda", "Accord", 2006));
    myCars.add(new Car("Ford", "Mustang", 1970));

    // Comparable が実装されているので、コンパレータなしでソート可能
    Collections.sort(myCars);

    for (Car c : myCars) {
      System.out.println(c.brand + " " + c.model + " " + c.year);
    }
  }
}

6. ソートに使える便利なトリック

2つの数値を自然にソートする際、通常は以下のように書きます:

if(a.year < b.year) return -1;
if(a.year > b.year) return 1;
return 0;

しかし、実はこれをたった1行で記述することができます:

return a.year - b.year;

このテクニックを利用すれば、逆順(降順)のソートも簡単です:

return b.year - a.year;

7. Comparator vs. Comparable

最後に、この2つのインターフェースの違いを整理します。

  • Comparator: 2つの異なるオブジェクトを比較するメソッド(compare)を持つ外部のオブジェクトです。
  • Comparable: 自分自身を他のオブジェクトと比較できる機能(compareTo)を持ったオブジェクトそのものです。

可能であれば Comparable インターフェースを使用する方が実装は簡単です。しかし、ソースコードを変更できない既存のクラスをソートしたい場合や、状況に応じて複数の異なるソート順を使い分けたい場合には、Comparator が非常に強力な武器となります。