JavaのComparator(比較インターフェース)について。
|
Comparatorは、“比較を行う関数”を表すインターフェース。
import java.util.Comparator;
ComparatorオブジェクトをCollections.
sort()・Arrays.
sort()メソッドやTreeMap・TreeSetのコンストラクター等に渡すことで、ソート順(並び替えの順序)を制御することが出来る。
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を実際にシリアライズしないなら関係ないけど)
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]
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を実装する必要がある。
Comparatorは関数型インターフェースである。
(Comparatorにはcompareとequalsの2つの抽象メソッドが宣言されているが、
equalsはObjectクラスにpublicで定義されているメソッドなので、関数型インターフェースの条件となる“抽象メソッド”としてはカウントしない為、
compareだけが宣言されていると見なされ、関数型インターフェースの条件を満たす)
(また、JDK1.8でComparatorにデフォルトメソッドやstaticメソッドが追加されたが、これも関数型インターフェースの条件としては無視される)
したがって、Comparatorを受け取るメソッドには、ラムダ式(やメソッド参照)を渡すことが出来る。
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()
); //ラムダ式
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]
「(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));
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);