S-JIS[2014-03-26/2014-05-18] 変更履歴

Java Comparator

JavaのComparator(比較インターフェース)について。


概要

Comparator(コンパレーター)は、“比較を行う関数”を表すインターフェース。

import java.util.Comparator;

ComparatorオブジェクトをCollections.sort()・Arrays.sort()メソッドやTreeMap・TreeSetのコンストラクター等に渡すことで、ソート順(並び替えの順序)を制御することが出来る。


実装方法(JDK1.7以前)

Comparatorインターフェースには、(抽象メソッドとして)compareとequalsが宣言されている。

public interface Comparator<T> {

	public int compare(T o1, T o2);

	public boolean equals(Object obj);
}

compareメソッドは引数を2つ受け取り、等しい場合は0、o1がo2より小さい場合(o1<o2)は負の数、o1がo2より大きい場合(o1>o2)は正の数を返すように実装する。

equalsメソッドがなぜ宣言されているのかは不明^^;
equals()はObjectクラスで実装されている為、特にオーバーライドする(自分で実装する)必要は無い。

また、ComparatorをTreeSetやTreeMap等のシリアライズ可能なクラスに渡す場合は、Comparatorを実装したクラスもシリアライズ可能にする(Serializableインターフェースを実装する)べきである。
(TreeSetやTreeMapを実際にシリアライズしないなら関係ないけど)


Listのソートの例(JDK1.7以前)

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
	List<String> list = Arrays.asList("a", "bcd", "ef");

	Collections.sort(list, new Comparator<String>() {
		@Override
		public int compare(String o1, String o2) {
			return o1.length() - o2.length(); //文字数で比較
		}
	});

	System.out.println(list);

↓出力結果

[a, ef, bcd]

TreeMapの例(JDK1.7以前)

import java.util.Map;
import java.util.TreeMap;
	Map<String, String> map = new TreeMap<>(new Comparator<String>() {
		@Override
		public int compare(String o1, String o2) {
			return o1.length() - o2.length(); //文字数で比較
		}
	});
	map.put("a", "1文字");
	map.put("bcd", "3文字");
	map.put("ef", "2文字");

	System.out.println(map);

↓出力結果

{a=1文字, ef=2文字, bcd=3文字}

シリアライズ可能にしたい場合は、匿名クラスでなく、別にクラスを定義してComparatorとSerializableを実装する必要がある。


実装方法(JDK1.8以降)

Comparatorは関数型インターフェースである。
(Comparatorにはcompareとequalsの2つの抽象メソッドが宣言されているが、
 equalsはObjectクラスにpublicで定義されているメソッドなので、関数型インターフェースの条件となる“抽象メソッド”としてはカウントしない為、
 compareだけが宣言されていると見なされ、関数型インターフェースの条件を満たす)
(また、JDK1.8でComparatorにデフォルトメソッドstaticメソッドが追加されたが、これも関数型インターフェースの条件としては無視される)

したがって、Comparatorを受け取るメソッドには、ラムダ式(やメソッド参照)を渡すことが出来る。


Listのソートの例

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
	List<String> list = Arrays.asList("a", "bcd", "ef");

	Collections.sort(list, (s1, s2) -> s1.length() - s2.length()); //ラムダ式

	System.out.println(list);

↓出力結果

[a, ef, bcd]

なお、JDK1.8から、Listにsortメソッドが追加された。[2014-05-18]
デフォルトの実装ではCollections.sort()を呼び出しているだけだが、オーバーライドできるので、Listの具象クラスによっては効率の良い実装になっている。

	List<String> list = Arrays.asList("a", "bcd", "ef");
	list.sort((s1, s2) -> s1.length() - s2.length()); //ラムダ式

TreeMapの例

import java.util.Map;
import java.util.TreeMap;
	Map<String, String> map = new TreeMap<>((s1, s2) -> s1.length() - s2.length()); //ラムダ式
	map.put("a", "1文字");
	map.put("bcd", "3文字");
	map.put("ef", "2文字");

	System.out.println(map);

↓出力結果

{a=1文字, ef=2文字, bcd=3文字}


※ラムダ式をシリアライズ可能にしたい場合は、交差型キャストでSerializableを実装させる。

	Map<String, String> map = new TreeMap<String, String>((Comparator<String> & Serializable) (s1, s2) -> s1.length() - s2.length());

応用例

Comparatorの色々な使用方法について。


大文字小文字を無視した文字列比較

	List<String> list = Arrays.asList("DEF", "abc", "123", "ghi");

	Collections.sort(list, String.CASE_INSENSITIVE_ORDER);
//	Collections.sort(list, (s1, s2) -> s1.compareToIgnoreCase(s2));

	System.out.println(list);

↓出力結果

[123, abc, DEF, ghi]

自然順ソート

Collections.sort()やArrays.sort()でComparatorを指定しない場合、ソート対象オブジェクトのクラスはComparableインターフェースを実装している必要があり、それを使ってソートされる。
これを「自然な順序」と呼んでいるらしい。

JDK1.8で、自然な順序の(Comparableを呼び出す)Comparatorが用意された。

	List<String> list = Arrays.asList("def", "abc", "ZZZ", "ghi");

	Collections.sort(list, Comparator.naturalOrder()); //StringはComparableを実装しているので、naturalOrder()が使える

	System.out.println(list);

↓出力結果

[ZZZ, abc, def, ghi]

逆順ソート

	List<String> list = Arrays.asList("def", "abc", "ZZZ", "ghi");

	Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
//	Collections.sort(list, Collections.reverseOrder(c)); //JDK1.5
	Collections.sort(list, c.reversed()); //JDK1.8

	System.out.println(list);

↓出力結果

[ghi, def, abc, ZZZ]

ソート対象オブジェクトのクラスがComparableインターフェースを実装している場合、それを使った逆順は以下の方法で実現できる。

	List<String> list = Arrays.asList("def", "abc", "ZZZ", "ghi");

//	Collections.sort(list, Collections.reverseOrder()); //StringはComparableを実装している
	Collections.sort(list, Comparator.reverseOrder()); //JDK1.8

	System.out.println(list);

↓出力結果

[ghi, def, abc, ZZZ]

nullを含むソート

(s1, s2) -> s1.compareTo(s2)」のようなラムダ式を使うと、ソート対象のList(の要素)にnullが含まれている場合はNullPointerExceptionが発生してしまう。
そこでnullsFirst()やnullsLast()メソッドを介在させるとnullもソートさせることが出来る。

	List<String> list = Arrays.asList("def", "abc", null, "ghi");
	Comparator<String> c = (s1, s2) -> s1.compareTo(s2);

	Collections.sort(list, Comparator.nullsFirst(c)); //nullを先頭にしてソート
	System.out.println(list);

	Collections.sort(list, Comparator.nullsLast(c)); //nullを末尾にしてソート
	System.out.println(list);

↓出力結果

[null, abc, def, ghi]
[abc, def, ghi, null]

キー項目によるソート

色々なプロパティー(フィールド)を持つオブジェクトのListに対し、一部のキー項目(フィールド)を使用してソートする例。

public class Data {
	private String key1;
	private int key2;
	private String value1;

	public Data(String key1, int key2, String value1) {
		this.key1 = key1;
		this.key2 = key2;
		this.value1 = value1;
	}

	public String getKey1() {
		return key1;
	}

	public int getKey2() {
		return key2;
	}

	public String getValue1() {
		return value1;
	}

	@Override
	public String toString() {
		return "Data(" + key1 + ", " + key2 + ", " + value1 + ")";
	}
}
	List<Data> list = Arrays.asList(new Data("def", 999, "foo"), new Data("abc", 777, "bar"), new Data("ghi", 111, "zzz"));

	Function<Data, String> key1Extractor = d -> d.getKey1(); // key1を抽出する関数
	Collections.sort(list, Comparator.comparing(key1Extractor)); //key1でソート
	System.out.println(list);

	ToIntFunction<Data> key2Extractor = d -> d.getKey2(); // key2を抽出する関数(key2はint)
	Collections.sort(list, Comparator.comparingInt(key2Extractor)); //key2でソート
	System.out.println(list);

↓出力結果

[Data(abc, 777, bar), Data(def, 999, foo), Data(ghi, 111, zzz)]
[Data(ghi, 111, zzz), Data(abc, 777, bar), Data(def, 999, foo)]

Comparator.comparing()に「キー項目を抽出する関数」を渡すと、キー項目で比較するComparatorを返す。
なお、抽出されたキー項目はComparableインターフェースを実装している必要がある。

キー項目がintの場合はcomparingIntメソッド(ToIntFunction)を使う。
同様にlong・double用のcomparingLong()・comparingDouble()メソッドがある。


キー項目の比較用のComparatorを別途渡すことも出来る。

	Function<Data, String> key1Extractor = d -> d.getKey1();       // key1を抽出する関数
	Comparator<String> key1Comparator = Comparator.reverseOrder(); // key1の比較方法
	Collections.sort(list, Comparator.comparing(key1Extractor, key1Comparator));

Comparatorの合成

2つのComparatorを合成して新しいComparatorを作ることが出来る。

thenComparingによってComparatorを合成すると、まず1つ目のComparatorを使って比較し、等しい場合は2つ目のComparatorを呼び出す、という動作になる。

	List<Data> list = Arrays.asList(new Data("def", 555, "foo"), new Data("abc", 777, "bar"), new Data("abc", 111, "zzz"), new Data("def", 555, "boo"));

	Comparator<Data> key1Comparator   = Comparator.comparing   (d -> d.getKey1());   // key1で比較する
	Comparator<Data> key2Comparator   = Comparator.comparingInt(d -> d.getKey2());   // key2で比較する(key2はint)
	Comparator<Data> value1Comparator = Comparator.comparing   (d -> d.getValue1()); // value1で比較する

	// key1,key2,value1で比較
	Comparator<Data> c = key1Comparator.thenComparing(key2Comparator).thenComparing(value1Comparator);

	Collections.sort(list, c);

	System.out.println(list);

↓出力結果

[Data(abc, 111, zzz), Data(abc, 777, bar), Data(def, 555, boo), Data(def, 555, foo)]

thenComparing()にも、抽出関数と比較関数を渡すメソッド(オーバーロード)や、
“int・long・double型で抽出する関数”を渡すメソッドがある。
ToIntFunctionを受け取るメソッドの名前がthenComparingIntなのがちょっと微妙だけど(「Int」が付いてなきゃいけないのか?)^^;

	List<Data> list = Arrays.asList(new Data("def", 555, "boo"), new Data("abc", 777, "zzz"), new Data("abc", 111, "zzz"), new Data("abc", 555, "boo"));

	Function<Data, String> key1Extractor   = d -> d.getKey1();   // key1を抽出する関数
	ToIntFunction<Data>    key2Extractor   = d -> d.getKey2();   // key2を抽出する関数(key2はint)
	Function<Data, String> value1Extractor = d -> d.getValue1(); // value1を抽出する関数

	// value1昇順、key1逆順、key2昇順
	Comparator<Data> c = Comparator.comparing(value1Extractor).thenComparing(key1Extractor, Comparator.reverseOrder()).thenComparingInt(key2Extractor);

	Collections.sort(list, c);

	System.out.println(list);

↓出力結果

[Data(def, 555, boo), Data(abc, 555, boo), Data(abc, 111, zzz), Data(abc, 777, zzz)]

上記の例では型を分かりやすくする為に一旦変数に代入したが、直接書く方が短く書ける。[2014-04-01]

	Comparator<Data> c = Comparator.comparing((Data d) -> d.getKey1()).
		thenComparingInt(d -> d.getKey2()).
		thenComparing(d -> d.getValue1());

メソッド参照を使ってさらに短く書くことが出来る。[2014-04-18]

	Comparator<Data> c = Comparator.comparing(Data::getKey1)
		.thenComparingInt(Data::getKey2)
		.thenComparing(Data::getValue1);

Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま