- ソート対象
- 昔ながらの読みにくいソート
- 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
を使わずとも、良いソート生活が送れますね。