Java Stream におけるソートのお作法


ソート対象

以下のような idname プロパティを持った 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 はやっぱり読みにくいですね。


comparing

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 を含む場合の扱いを指定できます。

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 を最後に持ってくることができます。


Map のソート

List ではなく Map をソートしたいケースもたまにあります。

Map のソートには Map.Entry.comparingByKeyMap.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);


idname をキーにソートしてみましょう。

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) でキー抽出 Function を指定してコンパレータを生成
  • Comparator.thenComparing(e) で複合条件を追加したコンパレータを生成


比較条件を指定する場合は第二引数に Comparator を渡します。

  • Comparator.comparing(e, c) でキー抽出 Function とソート条件指定の コンパレータを指定
  • Comparator.thenComparing(e, c) で複合条件を追加したコンパレータを生成(ソート条件指定)
  • Comparator.thenComparing(c) で複合条件を追加したコンパレータを生成
  • c.reversed() で順序を逆転したコンパレータを生成


条件指定に使うコンパレータは以下のものが用意されています。

  • naturalOrder() 自然順序付けのコンパレータ
  • reverseOrder() 逆順コンパレータ
  • nullsFirst(c) null を最小値とみなすコンパレータ(その他はコンパレータc の条件で順序付け)
  • nullsLast(c) null を最大値とみなすコンパレータ(その他はコンパレータcの条件で順序付け)


Map のソートには Map.Entry に以下が用意されています。

  • comparingByKey()
  • comparingByValue()
  • comparingByKey(c)
  • comparingByValue(c)


Guava の Ordering を使わずとも、良いソート生活が送れますね。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream