- ソート対象
- 昔ながらの読みにくいソート
- Lambda を利用した冗長なソート
- Java8 Stream における正しいソート
- thenComparing による複合キーのソート
- ソート条件の指定
- null コンパレータ
- Comparable
- Map のソート
- まとめ
ソート対象
以下のような id と name プロパティを持った Item を考えます。
public class Item { private Integer id; private String name; public Item(Integer id, String name) { this.id = id; this.name = name; } public Integer getId() { return id; } public String getName() { return name; } @Override public String toString() { return "Item{" + id + ", '" + name + "'}"; } }
以下のような Item のリストをソートしていきましょう。
List<Item> list = Arrays.asList(
new Item(9, "apple"),
new Item(3, "lemon"),
new Item(6, "peach"),
new Item(6, "banana"));
昔ながらの読みにくいソート
Comparator を定義してソートします。
List<Item> sorted = list.stream().sorted(new Comparator<Item>() { @Override public int compare(Item e1, Item e2) { return e1.getId().compareTo(e2.getId()); } }).collect(Collectors.toList()); // [Item{3, 'lemon'}, Item{6, 'peach'}, Item{6, 'banana'}, Item{9, 'apple'}]
Collections.sort() を使って直接コレクションを並び替えることもできます。
昔はよく見たコードですが、醜いですね。
Lambda を利用した冗長なソート
Comparator を Lambda で渡せば少しシンプルに書けます。
List<Item> sorted = list.stream()
.sorted((e1, e2) -> e1.getId().compareTo(e2.getId()))
.collect(Collectors.toList());
// [Item{3, 'lemon'}, Item{6, 'peach'}, Item{6, 'banana'}, Item{9, 'apple'}]
しかし引数2つの Lambda はやっぱり読みにくいですね。
Java8 Stream における正しいソート
java.util.Comparator インターフェースには、Comparator を生成するメソッドがあります。
これを使うのが Java8 Stream ソートの望ましい書き方になります。
List<Item> sorted = list.stream()
.sorted(Comparator.comparing(Item::getId))
.collect(Collectors.toList());
// [Item{3, 'lemon'}, Item{6, 'peach'}, Item{6, 'banana'}, Item{9, 'apple'}]
Comparator.comparing() の引数にソート用のキーを抽出する関数を渡すことで宣言的に書くことができます。
プリミティブの場合には Comparator.comparingInt() Comparator.comparingLong() といった専用のものが用意されています。
thenComparing による複合キーのソート
thenComparing() を使い、Comparator を連結することができます。
List<Item> sorted = list.stream()
.sorted(Comparator.comparing(Item::getId)
.thenComparing(Item::getName))
.collect(Collectors.toList());
// [Item{3, 'lemon'}, Item{6, 'banana'}, Item{6, 'peach'}, Item{9, 'apple'}]
第一ソートキーが id, 第二ソートキーが name でのソート例です。
ソート条件の指定
comparing は第二引数に Comparator を渡すことでソート条件を指定できます。
List<Item> sorted = list.stream()
.sorted(Comparator.comparing(Item::getId, Comparator.reverseOrder())
.thenComparing(Item::getName))
.collect(Collectors.toList());
// [Item{9, 'apple'}, Item{6, 'banana'}, Item{6, 'peach'}, Item{3, 'lemon'}]
reversed() で逆順を指定することもできます。
List<Item> sorted = list.stream()
.sorted(Comparator.comparing(Item::getId).reversed()
.thenComparing(Item::getName))
.collect(Collectors.toList());
// [Item{9, 'apple'}, Item{6, 'banana'}, Item{6, 'peach'}, Item{3, 'lemon'}]
null コンパレータ
ソートキーに null を含む場合は NullPointerException となります。
Comparator.nullsFirst() などで null 時の挙動を指定することで、NullPointerException を回避できます。
List<Item> sorted = list.stream()
.sorted(Comparator.comparing(Item::getId)
.thenComparing(Item::getName, Comparator.nullsFirst(Comparator.naturalOrder())))
.collect(Collectors.toList());
// [Item{3, 'lemon'}, Item{6, 'banana'}, Item{6, 'peach'}, Item{9, 'apple'}]
Comparator.nullsFirst(Comparator.naturalOrder()) で、null を先頭、その他は自然順に従うソートとなります。
Comparator.nullsLast() を使うと null を最後に持ってくることができます。
Comparable
オブジェクトに対してソート方法を規定できる場合は、Comparable を実装して以下のようにすることができます。
public class Item implements Comparable<Item> { // ... @Override public int compareTo(Item other) { return Comparator.comparing(Item::getId, Comparator.nullsFirst(Comparator.naturalOrder())) .thenComparing(Item::getName, Comparator.nullsLast(Comparator.naturalOrder())) .compare(this, other); } }
Comparable の実装があれば、ソートは単に以下のように実現できます。
List<Item> sorted = items.stream().sorted();
Map のソート
List ではなく Map をソートしたいケースもたまにあります。
Map のソートには Map.Entry.comparingByKey と Map.Entry.comparingByValue が用意されています。
以下の様な例で見ていきましょう。
Map<Item, Integer> map = new HashMap<>(); map.put(new Item(9, "apple"), 10); map.put(new Item(3, "lemon"), 12); map.put(new Item(6, "banana"), 8);
id と name をキーにソートしてみましょう。
Map<Item, Integer> sorted = map.entrySet().stream()
.sorted(Map.Entry.comparingByKey(
Comparator.comparing(Item::getId).thenComparing(Item::getName)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(e1, e2) -> e1, LinkedHashMap::new));
// {Item{3, 'lemon'}=12, Item{6, 'banana'}=8, Item{9, 'apple'}=10}
ソートキーが Comparable なオブジェクトであれば、単純に以下のように書くことができます。
Map<Item, Integer> sorted = map.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(e1, e2) -> e1, LinkedHashMap::new))
// {Item{6, 'banana'}=8, Item{9, 'apple'}=10, Item{3, 'lemon'}=12}
Entry の値でソートしています。
まとめ
java.util.Comparator インターフェースにはコンパレータを生成する以下のメソッドが用意されています。
ソートキーの指定
Comparator.comparing(e)でキー抽出関数eを指定してコンパレータを生成Comparator.thenComparing(e)で複合条件を追加したコンパレータを生成(eはキー抽出関数)
.sorted(Comparator.comparing(Item::getId))
.sorted(Comparator.comparing(Item::getId)
.thenComparing(Item::getName))
ソート条件の指定
比較条件を指定する場合は第二引数に Comparator を渡します。
Comparator.comparing(e, c)でキー抽出関数eとソート条件指定の コンパレータcを指定Comparator.thenComparing(e, c)で複合条件を追加したコンパレータを生成(ソート条件指定)Comparator.thenComparing(c)でコンパレータcを追加した複合コンパレータを生成c.reversed()で順序を逆転したコンパレータを生成
.sorted(Comparator.comparing(Item::getId,
Comparator.reverseOrder())
.sorted(Comparator.comparing(Item::getId).reversed()
.thenComparing(Item::getName,
Comparator.reverseOrder()))
ソート条件
条件指定に使うコンパレータは以下のものが用意されています。
naturalOrder()自然順序付けのコンパレータreverseOrder()逆順コンパレータnullsFirst(c)nullを最小値とみなすコンパレータ(その他はコンパレータcの条件で順序付け)nullsLast(c)nullを最大値とみなすコンパレータ(その他はコンパレータcの条件で順序付け)
.sorted(Comparator.comparing(Item::getId)
.thenComparing(Item::getName,
Comparator.nullsFirst(Comparator.naturalOrder())))
Map のソート
Map のソートには Map.Entry に以下が用意されています。
comparingByKey()キーでソートcomparingByValue()値でソートcomparingByKey(c)コンパレータcを指定してキーでソートcomparingByValue(c)コンパレータcを指定して値でソート
.sorted(Map.Entry.comparingByValue())
.sorted(Map.Entry.comparingByKey(
Comparator.comparing(Item::getId)
.thenComparing(Item::getName)))
Guava の Ordering を使わずとも、良いソート生活が送れますね。
