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 では sum
や average
のようなメソッドが利用できます。
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 など |
参考
- http://www.ne.jp/asahi/hishidama/home/tech/java/lambda.html:tile ラムダ式
- Java関数型インターフェースメモ(Hishidama's Java8 Functional Interface Memo) 関数型インターフェース
参考書籍
- Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで:書籍案内|技術評論社 全体的にこの本を学んだことを記載しています。