公開日:1/10/2021 更新日:3/26/2022
Java 8 から新たに、ラムダ式と StreamAPI という機能が追加されました。ラムダ式とはメソッドの引数に処理そのものを渡すことが出来るAPI記述に必須な記法です。StreamAPI とはコレクション操作を効率的に行えるように導入された機能です。従来の書き方のプログラミングは命令型プログラミングと呼ばれ、StreamAPIによる処理は関数型プログラミングと呼ばれます。本記事では、ネットや書籍で学んだ知識を備忘録としてまとめました。
ラムダ式とは、以下のような特殊な書き方をする記法です。
インターフェース名 変数 = (引数) -> {処理}
理解を深めるために、従来の記法を徐々にラムダ式へとリファクタリングして進めていきます。
①インナークラス → ②匿名クラス → ラムダ式 の順で修正します。
まずは、インターフェイスの Sample クラスを用意します。
//Sample.java
package com.sample;
public interface Sample {
public void execute();
}
次に、Factory クラスを用意します。こちらでは、インナークラス (InnerSampleImpl) のインスタンスを返却する generate メソッドを実装しています。背景として、InnerSampleImpl は外部からアクセス不可にするために、インナークラスとして実装されています。
//Factory.java
package com.sample;
public class Factory{
public Sample generate() {
return new Factory.InnerSampleImpl();
}
private class InnerSampleImpl implements Sample{
@Override
public void execute() {
System.out.println("test");
}
}
}
最後にメインクラスを用意します。ポリモフィズムを使用して、InnerSampleImple 型のインスタンスをSample 型の変数 sample に代入しています。
//Main.java
package com.main;
import com.sample.Factory;
import com.sample.Sample;
public class Main {
public static void main(String[] args) {
Sample sample= new Factory().generate();
sample.execute();
}
}
Factory で定義したインナークラスの InnerSampleImpl を、匿名クラスにリファクタリングしました。このようにクラス名が削除されて、処理の中身のみ記述されたクラスを匿名クラスと呼びます。
//Factory.java
package com.sample;
public class Factory{
public Sample generate() {
return new Sample(){
@Override
public void execute() {
System.out.println("test");
}
};
}
}
匿名クラスで記述された Factory をさらにラムダ式で簡略化すると以下のようになります。
//Factory.java
package com.sample;
public class Factory{
public Sample generate() {
return () ->{
System.out.println("test");
};
}
}
ラムダ式は、戻り値に設定されたインターフェイスにメソッドが一つだけ定義されている場合にのみ使用可能です。そのような実装すべきメソッドが一つしかないインターフェイスを、関数型インターフェイスと呼びます(Java8以降)。この例では、Sample クラスにexecute メソッド 1つしか定義されていなかったためラムダ式が使用できたわけです。上の例だけだと、「匿名クラスの実装が数行省略できて何が嬉しいのか?」と思うかもしれません。しかし、「インタフェースの実装をいろいろな宣言をふまえて書く代わりに、処理の内容を表す式だけ書けばいい」という特性が、後に出てくる Stream API と組み合わさった時に、コードの可読性向上に大いに役立ちます。
関数型インターフェース は自身で実装可能ですが、java.util.function では、あらかじめ必要になる関数型インターフェイスを提供してくれています。下記リンクから公式ドキュメントに飛べるので興味があれば覗いてください。
よく使用される、java.util.function パッケージに属する代表的な関数型インターフェイスを紹介します。メソッドの引数と戻り値によって場合分けした表がこちらです。
引数あり | 引数なし | |
---|---|---|
戻り値あり | Function , Predicate | Supplier |
戻り値なし | Consumer | × |
また、java.lang パッケージには 引数と戻り値なしの 関数型インターフェイスである Runnable 等もあります。
関数型インターフェイスはたくさんの種類があるようですね。
関数型インターフェイス (Consumer) の使用例
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> sample= (String s) ->{System.out.println(s);};
sample.accept("test");
//testと出力
}
}
Java8では既に用意されているメソッドを代入して、関数型インターフェイスのメソッドをオーバーライドをすることが可能です。使用ルールとしては、代入するメソッドの引数と戻り値が、代入先の関数インターフェイスの引数と戻り値に対応している必要があります。メソッド参照の書き方は以下の表の通りです。
用法 | 書き方 |
---|---|
インスタンスのメソッドを参照 | {インスタンス名}:{メソッド名} |
自分自身のインスタンスのメソッドを参照 | this::{メソッド名} |
staticメソッドを参照 | {クラス名}::{メソッド名} |
メソッド参照の使用例 1
//メソッド参照なし
Consumer<String> = s -> {System.out.println(s);}
s.accept("abc");
//メソッド参照あり
Consumer<String> = System.out::println;
s.accept("abc");
Consumer (引数1つで戻り値なしの 関数型インターフェイス) に 引数1つで戻り値なしの println() メソッドを参照代入している。accept メソッドを実行することで、System.out.println("abc") の結果が表示される。
String s = "abc";
IntSupplier supplier = s::length;
System.out.println(supplier.getAsInt());
例2では、IntSupplier (引数なし,戻り値1つでint型 の関数型インターフェイス ) に、引数なし 戻り値が1つで int型 のlengthメソッドを参照代入している。IntSupplier に定義された getAsInt() を実行することで、"abc".length() の結果が返ってくる。
Stream API は、大量データを逐次処理する「ストリーム処理」を効率的に記述するための手段として導入されました。Stream API は「作成」「中間操作」「終端操作」の3つの操作からできています。
操作 | 数 | 処理内容 |
---|---|---|
作成 | 最初に一つ | コレクションや配列などからStreamを作成する |
中間操作 | 複数 | Stream から Stream を作成する |
終端操作 | 最後に一つ | Stream からコレクションや配列へ変換したり、要素ごとに処理をしたり、要素を集計したりする |
List や Set からStream を作成するパターン
List<String> list = Arrays.asList("tanaka","kato","yamada");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
配列からStream を作成するパターン
//パターン1
String[] array = {"tanaka","kato","yamada"};
Stream<String> stream = Arrays.stream(array);
stream.forEach(System.out::println);
//パターン2
Stream<String> stream = Stream.of("tanaka","kato","yamada");
stream.forEach(System.out::println);
Map<String, String> map = new HashMap<>();
map.put("1","tanaka");
map.put("2","kato");
map.put("3","yamada");
Stream<Entry<String, String>> stream = map.entrySet().stream();
stream.forEach(e -> System.out.println(e.getksy() + ":" + e.getValue()));
メソッド名にmapが入った、要素を置き換える系の中間操作。
メソッド例:map, mapToInt, mapToDouble, mapToLong, flatMap
List<Student> students =new ArrayList<>();
students.add(new Student("tanaka",30));
students.add(new Student("kato",60));
students.add(new Student("yamada",100));
Stream<Integer> stream = students.stream()
//mapメソッドは Function (関数型インターフェイス) を引数に指定
.map(s -> s.getScore());//ラムダ式
.forEach(System.out::println);
要素を絞り込む中間操作
メソッド例:filter, limit ,distinct
List<Student> students =new ArrayList<>();
students.add(new Student("tanaka",30));
students.add(new Student("kato",60));
students.add(new Student("yamada",100));
Stream<Integer> stream = students.stream()
//filterメソッドは Predicate (関数型インターフェイス) を引数に指定
.filter(s -> s.getScore() > 50)//ラムダ式
.forEach(s -> System.out.println(s.getName()));
List<Student> students =new ArrayList<>();
students.add(new Student("tanaka",30));
students.add(new Student("kato",60));
students.add(new Student("yamada",100));
Stream<Integer> stream = students.stream()
//sorted メソッドは Comparator (関数型インターフェイス) を引数に設定
.sorted((s1,s2) -> s2.getScore() - s1.getScore()) //ラムダ式で高い順に並び替え
.forEach(s -> System.out.println(s.getName() + "" + s.getScore()));
1.繰り返し処理を行う終端操作
メソッド名 | 処理内容 | 引数 |
---|---|---|
forEach | このストリームの各要素に対してアクションを実行する | Consumer |
List<String> list = Arrays.asList("tanaka","kato","yamada");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
2.結果をまとめて出す終端操作
メソッド例:collect, toArray, reduce
メソッド名 | 処理内容 | 引数 | 戻り値 |
---|---|---|---|
collect | 要素を走査し、結果を作成 | Collector | 具象クラス |
toArray | 全要素を配列に変換する | なし | OptionalObject[] |
reduce | 値を集約する | BinaryOperator | Optional |
List<Student> students = new ArrayList<>();
students.add(new Student("tanaka",30));
students.add(new Student("kato",60));
students.add(new Student("yamada",100));
List<Student> newList = students.stream()
.filter(s -> s.getScore() > 50)
.collect(Collectors.toList());//Listにして返却
newList.forEach(s -> System.out.println(s.getName()));
//kato, yamada が出力
3.結果を一つだけ出す終端操作
メソッド名 | 処理内容 | 引数 | 戻り値 |
---|---|---|---|
findFirst | 先頭の要素を返す | なし | Optional |
findAny | いずれかの要素を返す | なし | Optional |
min | 最小の要素を返す | Comparator | Optional |
max | 最大の要素を返す | Comparator | Optional |
Optional (java.util.Optional) を戻り値として返す。Optional とは、オブジェクトの参照がnull かもしれないことを明示的に表すことが出来るクラス。Stream が空であったとき、空を返すことができる。
//例1
List<Integer> list = Arrays.asList(200, 100, 300);
Optional<Integer> s = list.stream().min(Comparator.naturalOrder());
System.out.println(s.get());
//100 が出力
//例2:オブジェクトのフィールド値で比較するパターン
List<Student> students = new ArrayList<>();
students.add(new Student("tanaka",30));
students.add(new Student("kato",60));
students.add(new Student("yamada",90));
Comparator<Student> scoreComparator = Comparator.comparing(Student::getScore);
Optional<Student> res = students.stream()
.max(scoreComparator);
System.out.println(res.get().getName());
//kato が出力
4.集計処理をおこなう終端操作
メソッド名 | 処理内容 | 戻り値 |
---|---|---|
count | 要素の個数を返す | long |
min | 最小の要素を返す | OptionalInt/OptionalDouble/OptionalLong |
max | 最大の要素を返す | OptionalInt/OptionalDouble/OptionalLong |
sum | 合計値を返す | OptionalInt/OptionalDouble/OptionalLong |
average | 平均値を返す | OptionalDouble |
List<Integer> list = Arrays.asList(200, 100, 300);
long s = list.stream().count();
System.out.println(s);
//3 が出力