読者です 読者をやめる 読者になる 読者になる

Java8のStream APIの使い方(中間操作編③ - sorted, peek)

Java
スポンサーリンク

Stream API 中間操作の sorted と peek について使い方をまとめました。

sorted:ソート

① 引数:なし / 戻り値:Stream<R>
② 引数:Comparator<T> / 戻り値:Stream<R>

sortedメソッドは2種類あり、①の場合は保持されてるデータがjava.lang.Comparableを実装してる必要があります。Comparable を実装してるクラスは  Java Platform SE 8 で確認して下さい。

②の場合は関数型インターフェースのComparator<T>を渡してやる事で、ソート順を制御でき、Comparableを実装してないクラスでもソートする事が可能です。

Comparator<T>は比較を行う関数型インターフェースであり、実装が必要なメソッドはint compare(T o1, T o2)で引数を2つ受け取り、intを返します。

sortedの使い方

Integerはjava.lang.Comparableを実装しているため引数なしでソート可能です。

List<Integer> lists = Arrays.asList(300, 100, 500, 200, 400);
lists.stream().sorted().forEach(System.out::print);

// 実行結果
100200300400500

リストに格納したPersonというクラスを年齢順ソートします。

public class Person {
    private String name;
    private String gender;
    private Integer age;

    public Person(String name, String gender, Integer age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    public String getName() { return name; }
    public String getGender() { return gender; }
    public Integer getAge() { return age; }
    public String toString() { return name + ", " + gender + ", " + age; }

    public static void main(String[] args) {
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("田中太郎", "male", 35));
        persons.add(new Person("山田一郎", "male", 22));
        persons.add(new Person("鈴木花子", "famale", 19));
        
        // ラムダ式 - 年齢でソート
        persons.stream().sorted((person1, person2) -> person1.getAge().compareTo(person2.getAge()))
                .forEach(Person::.toString);
    }
}

// 実行結果
鈴木花子, famale, 19
山田一郎, male, 22
田中太郎, male, 35

匿名クラスで記述した場合です。

        // 匿名クラス - 名前でソート
        persons.stream().sorted(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getName().compareTo(o2.getName());
            }
        }).forEach(person -> System.out.println(person.toString()));

        // 実行結果
        山田一郎, male, 22
        田中太郎, male, 35
        鈴木花子, famale, 19

Stream以外のsortメソッド

Java8からはList#sortメソッドが追加されたため Stream に変換しなくてもソートする事は可能です。引数にはComparator<T>を渡す必要があります。

  • List#sort(Comparator<? super E> c)
  • Collections.sort(List list)
  • Collections.sort(List list, Comparator<? super T> c)
  • Arrays.sort(T[] a, Comparator<? super T> c)

TreeMap のコンストラクタで Comparator を渡すとキーの並び順を指定できます。

Map<String, String> maps = new TreeMap<>((k1, k2) -> k1.length() - k2.length());

Comparatorのdefaultメソッド

Comparatorのデフォルトメソッドは以下の通りです。

メソッド 引数 戻り値
naturalOrder なし Comparator<T>
reverseOrder なし Comparator<T>
reversed なし Comparator<T>
nullsFirst Comparator<T> Comparator<T>
nullsLast Comparator<T> Comparator<T>
comparing Function<T, U> Comparator<T>
comparing Function<T, U>, Comparator<U> Comparator<T>
comparingInt ToIntFunction<T> Comparator<T>
comparingLong ToLongFunction<T> Comparator<T>
comparingDouble ToDoubleFunction<T> Comparator<T>
thenComparing Comparator<T> Comparator<T>
thenComparing Function<T, U> Comparator<T>
thenComparing Function<T, U>, Comparator<U> Comparator<T>
thenComparingInt ToIntFunction<T> Comparator<T>
thenComparingLong ToLongFunction<T> Comparator<T>
thenComparingDouble ToDoubleFunction<T> Comparator<T>

ソート順

ソート対象オブジェクトのクラスがComparableインターフェースを実装している場合、並び順の指定は以下のように行います。

自然順の場合は、Comparator#naturalOrderを使用。(実装されたComparableインターフェースを使った並び順)

lists.stream().sorted(Comparator.naturalOrder()).forEach(System.out::print);
// 実行結果
100200300400500

自然順の逆順でソートする場合は、Comparator#reverseOrderを使用。

lists.stream().sorted(Comparator.reverseOrder()).forEach(System.out::print);
// 実行結果
500400300200100

Comparableインターフェースを実装していないオブジェクトを逆順でソートする場合は、Comparator#reversedを使用。

Comparator<LamdaSample> c = (person1, person2) -> person1.getAge().compareTo(person2.getAge());
persons.stream().sorted(c.reversed()).forEach(person -> System.out.println(person.toString()));
// 実行結果
田中太郎, male, 35
山田一郎, male, 22
鈴木花子, famale, 19

Nullを含むソート

ソート対象のオブジェクトにNullが含まれる場合、NullPointerException が発生してしまいす。

List<String> strs = Arrays.asList("[hoge]", "[fuga]", null, "[bar]", "[foo]");
strs.stream().sorted().forEach(System.out::println);   // NullPointerExceptionが発生

このような場合はComparator#nullsFirestComparator#nullsLastを使用するとNullを含めたソートをすることが可能です。

strs.stream().sorted(Comparator.nullsFirst((s1, s2) -> s1.compareTo(s2))).forEach(System.out::print);
strs.stream().sorted(Comparator.nullsLast((s1, s2) -> s1.compareTo(s2))).forEach(System.out::print);
// 実行結果
null[bar][foo][fuga][hoge]
[bar][foo][fuga][hoge]null

キー項目を指定してソート

上述したサンプルにある Person というクラスをソートする場合、ソートするキー項目を指定して次のように記述する必要がありました。

persons.stream().sorted((person1, person2) -> person1.getAge().compareTo(person2.getAge())).forEach(person -> System.out.println(person.toString()));

これでは冗長で可読性も悪く毎回書くのも面倒です。こんな時にComparator#comparingを使用することでスッキリと記述する事が可能です。comparing は Function<T, U> を引数に取り、キー項目を抽出する関数を渡すとキー項目で比較する Comparator を返します。ただし、この場合Comparable#compareToを使用した Comparator を返すため、キー項目はComparableインターフェースを実装している必要があります。

// ラムダ式
persons.stream().sorted(Comparator.comparing(person -> person.getAge())).forEach(LamdaSample::toString);
// メソッド参照
persons.stream().sorted(Comparator.comparing(Person::getAge)).forEach(person -> System.out.println(person.toString()));
// 実行結果
鈴木花子, famale, 19
山田一郎, male, 22
田中太郎, male, 35

キー項目比較用のComparatorを渡す事も出来ます。

persons.stream().sorted(Comparator.comparing(Person::getAge, Comparator.reverseOrder())).forEach(person -> System.out.println(person.toString()));

キー項目がプリミティブ型の場合は、comparingInt comparingLong comparingDoubleを使用しましょう。

複数のソートキーを指定

Comparator#thenComparingを使用して複数の Comparator を使用する事も可能です。今回のソート対象は以下のクラスを使用します。

public class Staff {
    private String name;
    private String gender;
    private int age;
    private int salary;

    public Staff(String name, String gender, int age, int salary) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.salary = salary;
    }

    public String getName() { return name; }
    public String getGender() { return gender; }
    public int getAge() { return age; }
    public int getSalary() { return salary; }
    public String toString() { return name + ", " + gender + ", " + age + ", " + salary; }
}

次の例はSalary:降順、Age:降順、Name:昇順で並べ替えています。

List<Staff> staffs = new ArrayList<>();
staffs.add(new Staff("Michael", "male", 35, 400000));
staffs.add(new Staff("Jonson", "male", 35, 400000));
staffs.add(new Staff("William", "male", 23, 300000));
staffs.add(new Staff("Angelina", "famale", 29, 300000));

staffs.stream().sorted(Comparator.comparingInt(Staff::getSalary).reversed()
        .thenComparing(Comparator.comparingInt(Staff::getAge).reversed())
        .thenComparing(Comparator.comparing(Staff::getName)))
        .forEach(staff -> System.out.println(staff.toString()));

// 実行結果
Jonson, male, 35, 400000
Michael, male, 35, 400000
Angelina, famale, 29, 300000
William, male, 23, 300000

thenComparingにもキー項目を抽出する関数を渡す事や、プリミティブ型で抽出する関数を渡すメソッドも用意されています。

peek:特殊

引数:Consumer / 戻り値:Stream<T>

peekメソッドはStreamの状態を変えずにそのままStreamを返します。値を返さない Consumer<T> を引数に取るので forEach のようなイメージですが、peekメソッドは中間操作です。途中の状態を確認したりデバッグ目的で使用します。

peekの使い方

300未満の数値を filter で抽出して最後に数値の個数をカウントする処理で、抽出した結果を表示しています。

List<Integer> lists2 = Arrays.asList(333, 111, 555, 222, 444);
Long result = lists2.stream().filter(i -> i < 300).peek(t -> System.out.println("peek:" + t)).count();
System.out.println(result);

// 実行結果
peek:111
peek:222
2

関連記事

 Java8のforEachを使った繰り返し処理について - TASK NOTES

 Java8ラムダ式の使い方の基本 - TASK NOTES

 Java8のStream APIの使い方(Streamの生成編)

 Java8のStream APIの使い方(中間操作編① - filter, map)

 Java8のStream APIの使い方(中間操作編② - flatMap, distinct, limit, skip)

 Java8のStream APIの使い方(終端操作編① - anyMatch, allMatch, noneMatch) - TASK NOTES