技術メモ

技術メモ

ラフなメモ

Javaの基本事項を復習(StreamAPI・ラムダ式)

5 章: ストリーム処理

Stream API

  • Stream API は大量データを逐次処理する「ストレーム処理」を効率的に記述するための手段
  • 大量データではなくとも、コレクションの処理を効率的にできる
  • Stream API は「作成」「中間操作」「終端操作」を 3 つの操作から成り立っている

簡単な Stream API の例

Sample1_1.java

   public static void main(String[] args) {

        List<Student> student = new ArrayList<>();
        student.add(new Student("Ken", 100));
        student.add(new Student("Shin", 60));
        student.add(new Student("Tom", 80));

        student.stream()
            .filter(s -> s.getScore() >= 70)
            .forEach(s -> System.out.println(s.getName()));

    }

    @Data
    static class Student {
        String name;
        int score;

        public Student(String name, int score) {
            super();
            this.name = name;
            this.score = score;
        }
    }
    // Ken
    // Tom

ラムダ式

ラムダ式とは、「インターフェースの実装をいろいろな宣言を踏まえて書く代わりに、処理の内容を表す式のみを書けばいい」という特性があります。コードの可読性に影響することが考えられます。(あとは純粋に知らないと読めない)

ラムダ式を使ったソートの書き方

Sample1_2.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        Collections.sort(studentList ,new Comparator<Student>() {

            @Override
            public int compare(Student s1, Student s2) {
                return Integer.compare(s1.getScore(), s2.getScore());
            }

        } );

        System.out.println(studentList);

        Collections.sort(studentList, (s1, s2) -> -Integer.compare(s1.getScore(), s2.getScore()));

        System.out.println(studentList);
    }
    // [Student(name=Shin, score=60), Student(name=Tom, score=80), Student(name=Ken, score=100)]
    // [Student(name=Ken, score=100), Student(name=Tom, score=80), Student(name=Shin, score=60)]

関数型インターフェース

「関数型インターフェース」とは定義されている抽象メソッドが 1 つだけあるインターフェースのことです。static メソッドやデフォルトメソッドは含まれていても問題ないです。関数型インターフェースの条件としては無視されます。

パッケージjava.util.functionの説明 関数型インタフェースは、ラムダ式やメソッド参照のターゲットとなる型を提供します。各関数型インタフェースには、その関数型インタフェースの関数メソッドと呼ばれる単一の抽象メソッドが含まれており、ラムダ式のパラメータや戻り値の型のマッチングや適応は、そのメソッドに対して行われます。関数型インタフェースは代入の文脈、メソッド呼び出しの文脈、キャストの文脈など、さまざまな文脈でターゲットの型を提供できます。

ラムダ式は関数型インターフェースの代替として使用できます。

関数型インターフェースとして定義したい場合は @FunctionalInterface を付与すると、条件を満たしていない場合にコンパイルエラーとなります。(よって関数型インターフェースの場合はこのアノテーションを付与したほうがよい)

@FunctionalInterface
public interface FuncInterfaceExample {
    public int example(int n);
}

ラムダ式の基本文法

以下のような形が基本になります。

(引数) -> { 処理 }

見た目上は、無名内部クラス(匿名クラス)を短く記述できる記法と言えます。書き方のポイントは 2 つあって、引数の定義方法と処理の定義方法があります。

ラムダ式は関数型インターフェースのメソッドを実装(オーバーライド)することになります。関数型インターフェースの抽象メソッドが 1 つしかないため、ラムダ式がどのメソッドを実装すればよいか一意に決まります。

なので Sample2.java の例を考えると Student クラスの compare メソッドは比較する 2 つの引数をもつメソッドであり、丁寧に記述すると以下のようになります。

   Collections.sort(studentList, (Student s1, Student s2) -> {
        return -Integer.compare(s1.getScore(), s2.getScore());
    });

引数の省略

ラムダ式の引数の型は省略することができて、以下のようにしてよいです。

   Collections.sort(studentList, (s1, s2) -> {
        return -Integer.compare(s1.getScore(), s2.getScore());
    });

return{} も省略できて、以下のようにしてよいです。

   Collections.sort(studentList, (s1, s2) -> -Integer.compare(s1.getScore(), s2.getScore()));

さらに、引数の個数が 1 つの場合は変数を囲んでいる () も省略することができます。(x)x として良い、ということです。

以下の実装例を考えます。

Sample1_3.java

   public static void main(String[] args) {
        Consumer<String> consumer = (String s) -> { System.out.println(s);};
        consumer.accept("hoge");
    }
    // hoge

まずは型を省略できて以下のようにできます。

Sample1_4.java

   public static void main(String[] args) {
        Consumer<String> consumer = (s) -> { System.out.println(s);};
        consumer.accept("hoge");
    }
    // hoge

メソッドの引数が 1 つなので () も省略できます。

Sample1_5.java

   public static void main(String[] args) {
        Consumer<String> consumer = s -> { System.out.println(s);};
        consumer.accept("hoge");
    }
    // hoge

引数がない場合は () のみになります。Runnable インターフェースの run() は引数がないですが、その場合は以下のようになります。

   public static void main(String[] args) {
        Runnable runnable = () -> {System.out.println("Hello Lambda!");};
        runnable.run();
    }
    // Hello Lambda!

処理側の省略

ラムダ式の処理が 1 つしかないときは return{} を省略できます。

Sample1_6.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        Collections.sort(studentList,
            (s1, s2) -> {
                return -Integer.compare(s1.getScore(), s2.getScore());
            }
        );

        System.out.println(studentList);
    }

Sample1_6.java は以下のようにできます。

Sample1_7.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        Collections.sort(studentList,
            (s1, s2) -> -Integer.compare(s1.getScore(), s2.getScore())
        );

        System.out.println(studentList);
    }

メソッド参照

メソッドそのものを引数に渡すことができます。メソッド参照は「代入先の関数型インターフェースの引数の数と型が一致していれば、そこにメソッドを代入できる」というルールになっています。

  • forEach

    default void forEach(Consumer<? super T> action) Iterableの各要素に対して指定されたアクションを、すべての要素が処理されるか、アクションが例外をスローするまで実行します。

引数に Consumer インターフェースを持ちますが、メソッドの引数が 1 つです。System クラスが static に持つ標準出力ストリームインスタンス out のメソッドである println メソッドの引数の数と型が一致しているので代入することができます。

Sample1_8.java

   public static void main(String[] args) {

        List<String> list = Arrays.asList("xxx", "yyy", "zzz");
        list.forEach(System.out::println);

    }
    // xxx
    // yyy
    // zzz

ラムダ式で書くと以下になります。

Sample1_9.java

   public static void main(String[] args) {

        List<String> list = Arrays.asList("xxx", "yyy", "zzz");
        list.forEach(str -> System.out.println(str));
    }
    // xxx
    // yyy
    // zzz

メソッド参照は以下のように記述できます。

用法 記述
インスタンスのメソッドを参照 {インスタンス名}::{メソッド名}
自分自身のインスタンスのメソッドを参照 this::{メソッド名}
staticメソッドを参照 {クラス名}::{メソッド名}

Stream を作成する

List, Set から Stream を作成するには stream メソッドを利用します。

Sample2_1.java

   public static void main(String[] args) {
        List<String> list = Arrays.asList("xxx", "yyy", "zzz");
        Stream<String> stream = list.stream();
        stream.forEach(System.out::println);
    }

stream() で Stream インスタンスを作成します。Stream インスタンスはメソッドチェーン(メソッドを連続して呼び出すこと)を利用することが多いとのことです。

配列から Stream インスタンスを作成します。

Sample2_2.java

   public static void main(String[] args) {
        String[] array = {"xxx", "yyy", "zzz"};
        Stream<String> stream = Arrays.stream(array);
        stream.forEach(System.out::println);
    }
    // xxx
    // yyy
    // zzz

Map から Stream を作成します。

Java 8 には Map を適切に処理する Stream クラスはありません。Map に対する Stream 操作をしたい場合は Map の entrySet メソッドで Set を取得して、Set メソッドの stream メソッドを呼び出す必要があります。

Sample2_3.java

   public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "xxx");
        map.put(2, "yyy");
        map.put(3, "zzz");

        Stream<Entry<Integer, String>> stream = map.entrySet().stream();
        stream.forEach(e -> System.out.println(String.format("%d:%s", e.getKey(), e.getValue())));
    }
    // 1:xxx
    // 2:yyy
    // 3:zzz

数値範囲から Stream を作成します。

IntStream を用いることで数値範囲に対して Stream 処理することができます。

public interface IntStream extends BaseStream<Integer,IntStream> 順次および並列の集約操作をサポートするプリミティブint値要素のシーケンスです。これは、Streamに対してintプリミティブ特殊化を行ったものです。

Sample2_4.java

   public static void main(String[] args) {
        IntStream stream = IntStream.range(1, 5);
        stream.forEach(System.out::print);

        System.out.println("");

        stream = IntStream.rangeClosed(1, 5);
        stream.forEach(System.out::print);
    }
    // 1234
    // 12345

中間操作

ストリームインスタンス作成後、任意の中間操作ができる。代表的な操作は以下。

メソッド名 処理内容 戻り値
map 要素を別の値に置き換える Stream
mapToInt 要素を int 値に置き換える IntStream
flatMap 要素の Stream を結合する Stream

map メソッドでは Function インターフェースが用いられていますが、Function インターフェースは apply をメソッドにもつ関数型インターフェースです。

@FunctionalInterface public interface Function<T,R> 1つの引数を受け取って結果を生成する関数を表します。 これは、apply(Object)を関数メソッドに持つ関数型インタフェースです。

map の使い方はこんな感じ。

Sample3_1.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        Stream<Integer> stream = studentList.stream()
                .map(s -> s.getScore());
        stream.forEach(System.out::println);

    }
    // 100
    // 60
    // 80

実際は以下のように記述するみたいです。

studentList.stream().map(Student::getScore).forEach(System.out::println);

mapToInt()map メソッドと処理自体は同じだが、戻り値が IntStream となっています。これらの Stream では sumaverage のようなメソッドが利用できます。

Sample3_2.java

   public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        int sum = studentList.stream()
                .mapToInt(Student::getScore)
                .sum();
        System.out.println(sum);
    }
    // 240

flatMap メソッドは Stream を結合して 1 つの Stream として扱えるようにするためのメソッドです。

要素を絞り込む中間操作

要素を絞り込む中間操作の代表例について以下に記載します。要素の数は減りますが、要素の型が変わることはないです。

メソッド名 処理内容 引数
filter 条件に合致した要素のみに絞り込む Predicate
limit 指定した件数に絞り込む 数値
distinct ユニークな要素のみに絞り込む なし

filter のサンプルです。

Sample3_3.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        studentList.stream()
        .filter(s -> s.getScore() > 70)
        .forEach(s -> System.out.println(s.getName()));
    }
    // Ken
    // Tom

要素を並べ替える中間操作

代表的なメソッドは sorted です。sorted の引数には Comparator オブジェクトを指定します。

Sample3_4.java

   public static void main(String[] args) {

        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));

        studentList.stream()
            .sorted((s1, s2) -> Integer.compare(s2.getScore(), s1.getScore()))
            .forEach(s -> System.out.println(String.format("%s %d", s.getName(), s.getScore())));
    }
    // Ken 100
    // Tom 80
    // Shin 60

終端操作

繰り返し処理を行う終端操作

forEach は ストリームの各要素に対してアクションを実行します。引数は Consumer オブジェクトです。

結果をまとめて取り出す終端操作

メソッド名 処理内容 引数 戻り値
collect 要素を走査し、結果を作成する Collecotr 省略
toArray 全要素を配列に変換する なし OptionalObject[]
reduce 値を集約する BinaryOperator Optional

collect のサンプルです。

Smaple3_5.java

   public static void main(String[] args) {
        List<String> list = Arrays.asList("xxxxxx", "yyy", "zzzzzzzz");
        List<String> newList = list.stream()
                .filter(p -> p.length() > 5)
                .collect(Collectors.toList());
        System.out.println(newList);
    }
    // [xxxxxx, zzzzzzzz]

Collector の各メソッドは具象クラスを返却します。

groupingBy のサンプルです。

Sample3_6.java

   public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Ken", 100));
        studentList.add(new Student("Shin", 60));
        studentList.add(new Student("Tom", 80));
        studentList.add(new Student("John", 100));

        Map<Integer, List<Student>> map = studentList.stream()
                .collect(Collectors.groupingBy(Student::getScore));

        map.get(100).stream()
            .forEach(s -> System.out.println(s.getName()));

    }
    // Ken
    // John

結果を 1 つだけ取り出す終端操作

代表例のみ。

メソッド名 処理内容 引数 戻り値
findFirst 先頭の要素を返す なし Optional
findAny いずれかの要素を返す なし Optional
min 最小の要素を返す Comparator Optional
max 最大の要素を返す Comparator Optional

集計処理をおこなう終端操作

代表例のみ。

メソッド名 処理内容 引数 戻り値
count 要素の個数を返す なし long
sum 合計値を返す なし OptionalInt など
min 最小の要素を返す なし OptionalInt など

参考

参考書籍