JavaのStreamAPIを書きながら、そういえばこれってどういことなんだと
自分が疑問に思っていたことについて少しまとめてみました。
StreamAPIのtips集ではないので、そういうの期待されて記事を覗いた方は申し訳ありません。
この記事の概要
この記事を読めばこの要素について、ちょっと理解できる。
- 関数型インターフェース
- 中間処理と終端処理
この記事の目指すところ
なんとなく理解しているStreamAPIの文法的な要素を覚える。
そもそもStreamAPIの書き方って、どんなのだっけ。
1~10の偶数のみを表示するといった処理の場合のStreamAPIの書き方は下記のような形です。
Arrays.stream(new int[]{1,2,3,4,5,6,7,8,9,10})
.filter( i -> (i % 2) == 0 )
.forEach( i -> System.out.println(i));
(IntStream.rangeを使ったカッコいい形もありますが、そこは置いといて、、、)
ちなみに、従来通りのfor文とif文の書き方は下記のような形です。
for(int i : new int[]{1,2,3,4,5,6,7,8,10}) {
if((i % 2) == 0) {
System.out.println(i);
}
}
これぐらいの短い処理なら、行数的には同じだし変わりなく見えてしまいますよね。
しかし、複雑な処理になった場合、StreamAPIのfilterとかforEachだとかいうメソッドをしっかり覚えれば、
どこで何をやっているかが明確なので、見やすく理解しやすくなると個人的に思っています。
だけど、いきなり見るとこの引数ってなに?とかfilterとforEachの違いって?となるので、
少しまとめてみました。
関数型インターフェース
上のソースで見た引数の部分は、初めてだと見慣れないですよね。
filter( i -> (i % 2) == 0 )
実はこれは、関数型インターフェースを引数にもらう書き方で、ラムダ式というものに省略した書き方になっています。
java7以前を中心に業務をしている方は初めて見ると思います。
java8以降では関数型インターフェースの条件を満たしているものは関数の処理を引数に直接設定するラムダ式と
いう書き方ができます。
関数型インターフェースの定義とは?
大まかにいうと抽象メソッドが一つだけ定義されたインターフェースのことです。
実例だとこのようなものです。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
見ての通り、単なるインターフェースですよね。
抽象メソッドが一つだけということが重要で、この形の時のみ処理のみを記述するラムダ式という書き方ができます。
(defaultメソッドは書いてあっても問題ないです。)
ちなみに、FunctionalInterfaceは関数型インターフェースで使用可能と明示しているだけで
処理的に意味のあるものではありません。
関数型インターフェースをラムダ式で引数に設定する
filterメソッドを参考にしていくと、filterはPredicateインターフェースを引数に受け取ります。
Stream<T> filter(Predicate<? super T> predicate);
上のソースコードだと、こんな書き方をしていました。
filter( i -> (i % 2) == 0 )
Predicateの抽象メソッドはtestメソッドになります。
ので、引数を一つ受け取り、booleanを戻り値で返す処理をラムダ式で引数に指定してあげるだけです。
(実際に上の例がそれです。)
といっても、いきなり上の書き方がラムダ式ですと言われても、ラムダ式とは???ってなりますよね
軽く文法を見ていきましょう。
ラムダ式
ラムダ式の基本的な文法はこのような形です。
(引数) -> {処理}
アローの左側に引数を設定して、右側に処理を書くといった感じです。
つまり、今回書いたfilterのラムダ式を省略せずに見るとこのような形です。
filter( (int i) -> {return (i % 2) == 0;} )
この書き方なら初めて見る方でも、なんとなく判断がつくのではないでしょうか。
一番上のサンプルでは、引数の型とかを省略できるものを、全て省略しただけです。
(ラムダ式は省略のルールはまた今度、記事に起こそうかなと思います。)
簡単に言うと、引数の型と()を省略して、更にreturnステートメントと{}を省略しています。
全て条件付きですが、省略すると見通しが良くなりますよね。
文法さえ勉強すればわかるので、省略できる箇所は省略していくといいとおもいます。
関数型インターフェースの種類
関数型インターフェースは標準で様々なものが定義されています。
(ちなみに、EffectiveJavaでは自作の関数型インターフェース作ることを推奨していないので、
使用するときは、標準なものを使用することを心がけましょう。)
しかし、大別すると4つの系統にわけられます。
あとは、intとかlongとかプリミティブなもの専用だったり、引数が二つになったりとするものです。
基本となるものを覚えて、ケースバイケースで調べてみるのがおすすめです。
(数が多いので、私の頭では無理でした、、、)
とりあえず、基本となるSupplier、Consumer、Predicate、Functionの存在とメソッドを覚えていきましょう。
// Supplierインターフェース
// 引数を受け取らず、値を返却するgetメソッドを定義しています。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
// Consumerインターフェース
// 引数を受け取り、戻り値を返さないacceptメソッドを定義しています。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
// Predicateインターフェース
// 引数を受け取り、booleanを返却するtestメソッドを定義しています。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
};
// Functionインターフェース
// 任意の型の引数を受け取り、任意の型の戻り値を返却するapplyメソッドを定義しています。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
ちなみに、全ての種類を見たい人は「java.util.function」パッケージを調べて見るのがおススメです。
関数型インターフェースのまとめ
- 関数型インターフェースはラムダ式でかけて、スマートな見た目。
- 標準な関数型インテーフェースを覚えるのが重要。
中間処理と終端処理
StreamAPIの処理の流れは主に3つの流れに区分されます。
1. streamの生成
今回もArrays.stream()でstreamを生成しています。streamを生成するだけなので、特に詳しく説明しません。
2. 中間処理
今回の場合、filterが該当します。中間処理は一つのStreamの中で何回もすることができます。
3. 終端処理
今回の場合、forEachが該当します。終端処理は一つのStreamで一回しか呼び出すことができません。
StreamAPIの処理の流れは生成→中間処理→終端処理になります。
しかし、なぜ、中間処理が複数回の実行ができて、終端メソッドは一回だけなのでしょうか。
よくある説明のこれだけの情報だと混乱してしまいます。
中間処理と終端処理を区物していく
正直言うと、中間処理と終端処理のメソッドだけ覚えて区別していくと大変です。
(何個覚えればいいんだ、、、、、)
メソッドを覚えるよりも、中間処理と終端処理の流れを理解するのが重要です。
何回も、登場しますが、また同じソースコードを。
Arrays.stream(new int[]{1,2,3,4,5,6,7,8,9,10})
.filter( i -> (i % 2) == 0 )
.forEach( i -> System.out.println(i));
ここで重要なのが、filterメソッドの後にforEachメソッドを呼び出せているということです。
ちなみに、こんな書き方しても動きません。
// ダメな例(というか、コンパイルエラー)
Arrays.stream(new int[]{1,2,3,4,5,6,7,8,9,10})
.forEach( i -> System.out.println(i));
.filter( i -> (i % 2) == 0 )
実はfilterメソッドの後にforEachメソッドが呼び出せる理由は、Streamインターフェースを見れば直に理解できます。
Streamインターフェースで理解する中間処理と終端処理
下のソースコードはStreamインターフェースの抽象メソッドを抜き出したものです。
public interface Stream<T> extends BaseStream<T, Stream<T>> {
// 戻り値がStream
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>>
mapper);
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function<? super T, ? extends LongStream>
mapper);
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream>
mapper);
Stream<T> distinct();
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
Stream<T> peek(Consumer<? super T> consumer);
Stream<T> limit(long maxSize);
Stream<T> substream(long startInclusive);
Stream<T> substream(long startInclusive, long endExclusive);
// 戻り値がいろいろ
void forEach(Consumer<? super T> action);
void forEachOrdered(Consumer<? super T> action);
Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
<R> R collect(Supplier<R> resultFactory,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R> R collect(Collector<? super T, R> collector);
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
long count();
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
Optional<T> findFirst();
Optional<T> findAny();
}
よく見るとfilterメソッドって、Streamを戻り値にしています。
そして、forEachは戻り値がないですよね。
これです!(どれだよ)
これはインターフェースですけど、実装クラス「ReferencePipeline」とか見てみると
filterメソッドなど中間メソッドたちは、メソッド内でStreamを新しく作って、戻り値に設定しています。
Streamを戻り値に設定しているから、連続してStreamメソッドを呼び出せるのです。
逆に、メソッドの戻り値を見てわかる通り、forEachなどの終端メソッドは戻り値にStreamを設定していないので、
連続してStreamメソッドを呼び出せないのです。
これを理解すると中間メソッドと終端メソッドってすごく簡単ですよね。
Streamを戻り値にしているメソッドが中間処理で
Streamを戻り値にしていないのが終端処理と大雑把に理解することができます。
中間処理と終端処理のまとめ
- 中間処理はStreamを戻り値にしているから何回も呼び出せる。
- 終端処理はStream以外を戻り値にしているから一回しか呼び出せない。
まとめ
- 関数型インターフェースを理解してラムダの文法を覚えれば引数が怖くない。
- 中間処理と終端処理って大層な名前だけど戻り値が違うだけ
理解すればStreamAPIって怖くない。
TODO
- メソッド参照で引数設定できるって聞いたけど。
- なんかいろんな実例が欲しい。